「這支測試昨天明明是綠燈的,今天怎麼突然紅了?」
如果你的測試依賴「現在幾點」來決定行為——例如抓「過去兩天」的資料、判斷某筆紀錄是不是「已經超過一週沒更新」——卻沒有特別處理,你的測試結果就會偷偷跟著系統時鐘一起漂移。今天要拆一個真實案例:一支負責抓外部資料的 Job,怎麼把「現在幾點」這個變數鎖死,讓測試結果不再看日曆臉色。
Carbon::setTestNow() 把測試裡的「現在」鎖定成固定時間點這個系統有一支負責定期抓外部新聞資料的 Job,它的核心邏輯是「抓某個時間區間內的新聞」——呼叫端傳入 start、end 兩個時間參數,Job 內部再用這兩個時間去跟外部服務要資料。乍看之下,這支 Job 的行為只跟傳進去的參數有關,不該跟「現在幾點」扯上關係。
但實際上,很多依賴時間區間的邏輯,內部還是會用「現在」做一些隱性判斷——例如某筆資料的「有效期限」、某個排程任務「下一次該抓的區間」,這些計算如果沒有鎖定測試當下的時間,測試結果就會隨著執行的日期悄悄改變。
it('handle', function () {
$client = new FakeHttpClient(fixture_path('vcr/jobs/fetch_news.yaml'));
$crawler = new Crawler(client: $client, baseUrl: 'http://internal.example.com/proxy');
swap(Crawler::class, $crawler);
$start = Carbon::parse('2023-08-14');
$end = Carbon::parse('2023-08-16');
FetchNewsJob::dispatchSync(start: $start, end: $end);
assertDatabaseCount('news', 13);
});
如果 Job 內部有任何一段邏輯間接依賴 now()(例如判斷「這筆資料是不是還在有效期內」、記錄「這次抓取的時間戳記」),這支測試在不同天執行,可能會得到不同的結果——今天綠燈,過幾天卻莫名其妙紅燈,而你完全不知道為什麼,因為程式碼跟測試資料看起來都沒變。
beforeEach(function () {
Carbon::setTestNow(Carbon::parse('2023-08-22'));
});
it('handle', function () {
$client = new FakeHttpClient(fixture_path('vcr/jobs/fetch_news.yaml'));
$crawler = new Crawler(client: $client, baseUrl: 'http://internal.example.com/proxy');
swap(Crawler::class, $crawler);
seed(NewsCategorySeeder::class);
seed(NewsAssignerSeeder::class);
$start = Carbon::parse('2023-08-14');
$end = Carbon::parse('2023-08-16');
FetchNewsJob::dispatchSync(start: $start, end: $end);
assertDatabaseCount('news', 13);
assertDatabaseHas('news', [
'id' => 1,
'news_category_id' => 8,
'published' => false,
]);
});
在 beforeEach 裡呼叫 Carbon::setTestNow(),把整個測試執行期間 Laravel 的「現在」固定成一個明確的日期。這樣一來,不管 Job 內部有沒有隱性依賴 now(),測試結果都不會因為「今天是幾號」而改變——這支測試明年、後年重跑,結果都會跟今天一模一樣,因為它已經跟真實世界的時鐘脫鉤了。
除了時間鎖定,這支測試還有兩個值得注意的細節:
第一,它用 FakeHttpClient 搭配一份 YAML 格式的 cassette 檔案,把 Crawler 依賴的 HTTP client 換成假的(透過 swap() 綁定容器),讓整支測試完全不會真的打外部服務——這正是前面 Day 16/17 拆過的測試替身機制在這裡的實際應用。
第二,也是最容易被忽略的一點:它不是只驗證「Job 執行完沒有丟例外」,而是用 assertDatabaseCount() 跟 assertDatabaseHas() 直接檢查資料庫裡的最終狀態——不只驗證「有 13 筆新聞被寫入」,還驗證了其中一筆的欄位內容是不是真的符合預期(分類 ID、發布狀態)。Job 跑完沒有報錯,不代表它把資料寫對了;只有真的去檢查資料庫裡的最終狀態,才知道這個 Job 真正做了什麼。
回想你維護的專案裡,有沒有一支測試依賴 now()、today() 或任何跟「現在」有關的判斷,卻沒有用類似 Carbon::setTestNow() 的機制鎖定?如果有,下次你的測試在奇怪的時間點突然變紅,這可能就是原因。
Carbon::setTestNow() 能把測試期間的「現在」固定成明確時間點,讓測試結果永遠可預測明天要看這個系統裡有 6 支結構高度相似的 SOAP 同步 Job,它們共用同一套測試模式——把這套模式拆開來看,理解「一套模式重複套用」跟「隔離每支測試互不干擾」是兩件不同的事。